﻿//-----------------------------------------------------------------------
// <copyright file="ExplorerItemViewModel.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>15/07/2012</date>
//-----------------------------------------------------------------------
using ConsoleHoster.Common.UserInput;
using ConsoleHoster.Common.Utilities;
using ConsoleHoster.Common.Utilities.IO;
using ConsoleHoster.Common.Utilities.Threading;
using ConsoleHoster.Common.ViewModel;
using ConsoleHoster.Model;
using ConsoleHoster.Model.Entities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;

namespace ConsoleHoster.ViewModel.Entities
{
	[DebuggerDisplay("{ExplorerItem.Name}")]
	public sealed class ExplorerItemViewModel : ViewModelBase, IMouseInputHandler, IDisposable
	{
		private readonly ExplorerItem explorerItem;
		private readonly ExplorerItemViewModel parent;
		private readonly IKeyboardStateProvider keyboardStateProvider;
		private readonly IConsolePluginApi consoleVM;
		private readonly IProcessManager processManager;

		private ObservableCollection<ExplorerItemViewModel> children = null;
		private ExplorerItemState itemState = ExplorerItemState.Collapsed;
		private bool isSelected = false;
		private string navigateToFolder = null;

		private Action<ExplorerItemViewModel> OnSelectedCallback;
		private readonly Object itemsLoadingSyncRoot = new Object();
		private CancellationTokenSource loadTaskCancelationSource = null;
		private Task itemsLoadingTask = null;

		private readonly IFileSystemContentProvider directoryContentProvider;

		public ExplorerItemViewModel(
			ExplorerItem argItem,
			Action<ExplorerItemViewModel> argSelectedCallback,
			IConsolePluginApi argConsoleApi,
			IKeyboardStateProvider argKeyboardStateProvider,
			IDispatcher argDispatcher,
			IProcessManager argProcessManager,
			IFileSystemContentProvider argDirectoryContentProvider,
			ILogger argLogger)
			: base(argDispatcher, new ComponentLogger("ExplorerItemViewModel", argLogger))
		{
			ValidationManager.RequireArgumentNotNull(argItem, "argItem");
			ValidationManager.RequireArgumentNotNull(argConsoleApi, "argConsoleApi");
			ValidationManager.RequireArgumentNotNull(argKeyboardStateProvider, "argKeyboardStateProvider");
			ValidationManager.RequireArgumentNotNull(argProcessManager, "argProcessManager");
			ValidationManager.RequireArgumentNotNull(argDirectoryContentProvider, "argDirectoryContentProvider");

			this.directoryContentProvider = argDirectoryContentProvider;
			this.processManager = argProcessManager;
			this.consoleVM = argConsoleApi;
			this.keyboardStateProvider = argKeyboardStateProvider;

			this.explorerItem = argItem;
			if (this.ExplorerItem.ItemType != ExplorerItemType.File)
			{
				this.SetDummyChildOnly();
			}
			this.OnSelectedCallback = argSelectedCallback;
		}

		private ExplorerItemViewModel(
			ExplorerItem argItem,
			Action<ExplorerItemViewModel> argSelectedCallback,
			ExplorerItemViewModel argParent,
			IConsolePluginApi argConsoleApi,
			IKeyboardStateProvider argKeyboardStateProvider,
			IDispatcher argDispatcher,
			ILogger argLogger)
			: this(argItem, argSelectedCallback, argConsoleApi, argKeyboardStateProvider, argDispatcher, new ProcessManager(), new FileSystemContentProvider(), argLogger)
		{
			this.parent = argParent;
		}

		public void NavigateTo(string argFolder)
		{
			Task.Factory.StartNew(async () =>
				{
					await this.NavigateToAsync(argFolder);
				});
		}

		public bool IsParentOf(ExplorerItemViewModel argItem)
		{
			ExplorerItemViewModel tmpParent = argItem.Parent;
			while (tmpParent != null && tmpParent != this)
			{
				tmpParent = tmpParent.Parent;
			}

			return tmpParent == this;
		}

		public bool HandleMouseInput(MouseButton argButton, MouseButtonState argState, int argClickCount, string argContext)
		{
			bool tmpResult = false;

			if (argButton == MouseButton.Left && argState == MouseButtonState.Released && argClickCount == 1)
			{
				if (this.keyboardStateProvider.AnyOfKeysDown(Key.LeftCtrl, Key.RightCtrl))
				{
					this.consoleVM.AppendCommand(this.ExplorerItem.Path);
					tmpResult = true;
				}
				else if (this.keyboardStateProvider.AnyOfKeysDown(Key.LeftAlt, Key.RightAlt))
				{
					this.consoleVM.AppendCommand(this.ExplorerItem.Name);
					tmpResult = true;
				}
				else if (this.keyboardStateProvider.AnyOfKeysDown(Key.LeftShift, Key.RightShift))
				{
					tmpResult = this.OnFolderChanged();
				}
			}

			if (argButton == MouseButton.Left && argState == MouseButtonState.Pressed && argClickCount == 2)
			{
				if (this.ExplorerItem != null && this.ExplorerItem.ItemType == ExplorerItemType.File)
				{
					this.Run();
					tmpResult = true;
				}
			}

			if (argButton == MouseButton.Right && argState == MouseButtonState.Released)
			{
				if (this.keyboardStateProvider.AnyOfKeysDown(Key.LeftCtrl, Key.RightCtrl))
				{
					if (this.ExplorerItem != null)
					{
						this.OpenExplorer();
						tmpResult = true;
					}
				}
				else if (this.keyboardStateProvider.AnyOfKeysDown(Key.LeftAlt, Key.RightAlt))
				{
					if (this.ExplorerItem != null)
					{
						this.OnCreateFavorite();
						tmpResult = true;
					}
				}
			}

			return tmpResult;
		}

		public void OpenExplorer()
		{
			try
			{
				this.processManager.StartProcess("explorer.exe", this.ExplorerItem.Path);
			}
			catch
			{
				this.consoleVM.RunCommand(StringUtilities.Format("start \"{0}\"", this.ExplorerItem.Path));
			}
		}

		public bool Run()
		{
			bool tmpResult = false;
			ExplorerItem tmpItem = this.ExplorerItem;
			if (tmpItem.ItemType == ExplorerItemType.File)
			{
				string tmpCommandToRun = tmpItem.Path.Contains(' ') ? String.Format("\"{0}\"", tmpItem.Path) : tmpItem.Path;
				this.consoleVM.RunCommand(tmpCommandToRun);
				tmpResult = true;
			}

			return tmpResult;
		}

		public void OnCreateFavorite()
		{
			ExplorerItem tmpItem = this.ExplorerItem;
			string tmpCommandText;
			string[] tmpFolders = tmpItem.Path.Split(new[] { '\\' });
			if (!tmpFolders[tmpFolders.Length - 1].Contains('.'))
			{
				tmpCommandText = CommandsBuilder.GetRedirectCommand(tmpItem.Path);
			}
			else
			{
				tmpCommandText = String.Format("\"{0}\"", tmpItem.Path);
			}

			this.consoleVM.AddFavoriteCommand(tmpItem.Name, tmpCommandText);
		}

		public bool OnFolderChanged()
		{
			bool tmpResult = this.ExplorerItem.ItemType == ExplorerItemType.Folder;
			if (tmpResult)
			{
				this.consoleVM.RunCommand(CommandsBuilder.GetRedirectCommand(this.ExplorerItem.Path));
			}

			return tmpResult;
		}

		public async void Dispose()
		{
			await this.StopBackgroundLoadIfAny();
		}

		#region Helper methods
		internal async Task NavigateToAsync(string argFolder)
		{
			if (this.NavigateToFolder == argFolder)
			{
				return;
			}

			if (!this.ExplorerItem.Path.Equals(argFolder, StringComparison.InvariantCultureIgnoreCase))
			{
				this.NavigateToFolder = argFolder;
			}
			else
			{
				this.IsSelected = true;
				this.OnSelectedCallback(this);
			}

			if (!this.IsFile)
			{
				await this.ExpandAsync();
			}
		}

		private async Task BeginLoadChildItems()
		{
			if (this.ItemState != ExplorerItemState.Collapsed)
			{
				return;
			}

			await this.StopBackgroundLoadIfAny();

			this.Logger.LogMessage("Emptying child list for item: {0}", this.ExplorerItem.Path);
			this.Children = new ObservableCollection<ExplorerItemViewModel>();
			this.ItemState = ExplorerItemState.Expanding;

			Task tmpItemsLoadingTask = null;
			CancellationTokenSource tmpLoadCancellation = new CancellationTokenSource();
			lock (this.itemsLoadingSyncRoot)
			{
				this.loadTaskCancelationSource = tmpLoadCancellation;
				tmpItemsLoadingTask = this.LoadChildItemsAsync(tmpLoadCancellation.Token);
				this.itemsLoadingTask = tmpItemsLoadingTask;
			}

			await tmpItemsLoadingTask;
		}

		private void SetDummyChildOnly()
		{
			this.Children = new ObservableCollection<ExplorerItemViewModel>(new List<ExplorerItemViewModel> { null });
		}

		protected override void OnErrorStateChanged()
		{
			this.NotifyPropertyChange("ItemStateColor");
		}

		private async Task StopBackgroundLoadIfAny()
		{
			CancellationTokenSource tmpCancellationSource;
			Task tmpLoadingTask;
			lock (this.itemsLoadingSyncRoot)
			{
				tmpCancellationSource = this.loadTaskCancelationSource;
				this.loadTaskCancelationSource = null;

				tmpLoadingTask = this.itemsLoadingTask;
				this.itemsLoadingTask = null;
			}

			if (tmpLoadingTask != null)
			{
				this.Logger.LogWarning("Background task cancellation requested. Stopping asynchronous items loading task.");
				tmpCancellationSource.Cancel();

				await tmpLoadingTask;
			}
		}

		private async Task LoadChildItemsAsync(CancellationToken argCancellationToken)
		{
			this.Logger.LogMessage("Starting loading children for item: {0}", this.ExplorerItem.Path);

			try
			{
				if (await this.AddFileSystemItemsToTree(this.directoryContentProvider.EnumerateFolders(this.ExplorerItem.Path), argCancellationToken, ExplorerItemType.Folder))
				{
					await this.AddFileSystemItemsToTree(this.directoryContentProvider.EnumerateFiles(this.ExplorerItem.Path), argCancellationToken, ExplorerItemType.File);
				}

				await this.FinishedLoadingFolderItems();
			}
			catch (Exception ex)
			{
				this.ItemState = ExplorerItemState.Collapsed;

				string tmpErrorMessage = String.Format("Explorer item '{0}' was unable to load children", this.ExplorerItem.Path);
				this.Logger.LogError("{0}. Details: {1}", tmpErrorMessage, ex.ToString());
				this.ErrorMessage = tmpErrorMessage;
			}
		}

		private async Task<bool> AddFileSystemItemsToTree(IEnumerable<string> argItems, CancellationToken argCancellatinoToken, ExplorerItemType argItemType)
		{
			bool tmpResult = true;
			foreach (string tmpPath in argItems)
			{
				if (argCancellatinoToken.IsCancellationRequested)
				{
					this.Logger.LogWarning("Cancellation requested while adding explorer item '{0}' to the explorer tree. Cancelling the operation.", tmpPath);
					tmpResult = false;
					break;
				}

				await this.AddLoadedChildItemAsync(argItemType, tmpPath);
			}

			return tmpResult;
		}

		private async Task AddLoadedChildItemAsync(ExplorerItemType argItemType, string argPath)
		{
			ExplorerItem tmpItem = new ExplorerItem(argItemType)
			{
				Name = argPath.Split(new char[] { '\\' }).Last(),
				Path = argPath
			};
			ExplorerItemViewModel tmpVM = new ExplorerItemViewModel(tmpItem, this.OnSelectedCallback, this, this.consoleVM, this.keyboardStateProvider, this.Dispatcher, this.Logger);

			await this.RunActionOnUIThread(() =>
				{
					this.Children.Add(tmpVM);
				}, true, DispatcherPriority.Background);
		}

		private async Task FinishedLoadingFolderItems()
		{
			this.Logger.LogMessage("Finished loading explorer items for item {0}", this.ExplorerItem.Path);
			this.ItemState = ExplorerItemState.Expanded;

			await this.OnFolderExpanded();
		}

		private async Task OnFolderExpanded()
		{
			if (this.NavigateToFolder != null && this.NavigateToFolder != this.ExplorerItem.Path)
			{
				this.Logger.LogMessage("Expanding explorer item {0}", this.ExplorerItem.Path);

				await this.RunAsyncAction(async () =>
					{
						string[] tmpPathLeftSections = this.NavigateToFolder.Remove(0, this.ExplorerItem.Path.Length).Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
						if (tmpPathLeftSections.Any())
						{
							string tmpChildFolder = tmpPathLeftSections[0];
							ExplorerItemViewModel tmpChildVM = this.Children.Where(item => String.Equals(item.ExplorerItem.Name, tmpChildFolder, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
							if (tmpChildVM != null)
							{
								await tmpChildVM.NavigateToAsync(this.NavigateToFolder);
							}
						}
						this.NavigateToFolder = null;
						this.Logger.LogMessage("Expanding folder succeeded for item {0}", this.ExplorerItem.Path);
					}, false, null, true);
			}
		}

		public void Expand()
		{
			Task.Factory.StartNew(async () => await this.ExpandAsync());
		}

		internal async Task ExpandAsync()
		{
			if (this.IsFile)
			{
				throw new ApplicationException("File item cannot be expanded");
			}

			if (this.IsExpanded)
			{
				await this.OnFolderExpanded();
			}
			else
			{
				await this.BeginLoadChildItems();
			}
		}

		public async void Collapse()
		{
			if (this.IsFile)
			{
				throw new ApplicationException("File item cannot be collapsed");
			}

			await this.StopBackgroundLoadIfAny();
			this.SetDummyChildOnly();
			this.ItemState = ExplorerItemState.Collapsed;
		}
		#endregion

		#region Properties
		public ObservableCollection<ExplorerItemViewModel> Children
		{
			get
			{
				return this.children;
			}
			set
			{
				this.SetPropertyValue("Children", ref this.children, value);
			}
		}

		public ExplorerItem ExplorerItem
		{
			get
			{
				return this.explorerItem;
			}
		}

		public ExplorerItemState ItemState
		{
			get
			{
				return this.itemState;
			}
			set
			{
				if (this.SetPropertyValue("ItemState", ref this.itemState, value))
				{
					this.NotifyPropertyChange("IsExpanded");
				}
			}
		}

		public bool IsExpanded
		{
			get
			{
				return this.ItemState != ExplorerItemState.Collapsed;
			}
			set
			{
				if (!this.IsFile)
				{
					if (value)
					{
						this.Expand();
					}
					else
					{
						this.Collapse();
					}
				}
			}
		}

		public bool IsSelected
		{
			get
			{
				return this.isSelected;
			}
			set
			{
				if (value != this.isSelected)
				{
					this.isSelected = value;
					this.NotifyPropertyChange("IsSelected");
				}
			}
		}

		public bool IsFile
		{
			get
			{
				return this.ExplorerItem.ItemType == ExplorerItemType.File;
			}
		}

		public bool IsNotFile
		{
			get
			{
				return !this.IsFile;
			}
		}

		public ExplorerItemViewModel Parent
		{
			get
			{
				return this.parent;
			}
		}

		public Brush ItemStateColor
		{
			get
			{
				return new SolidColorBrush(String.IsNullOrWhiteSpace(this.ErrorMessage) ? Colors.Black : Colors.Red);
			}
		}

		private string NavigateToFolder
		{
			get
			{
				return this.navigateToFolder;
			}
			set
			{
				this.navigateToFolder = value;
			}
		}
		#endregion

		public enum ExplorerItemState
		{
			Collapsed,
			Expanding,
			Expanded
		}
	}
}